namespace Craftsman.Builders;

using System;
using System.Collections.Generic;
using Domain;
using Domain.Enums;
using Helpers;
using Services;
using MediatR;

public static class EntityBuilder
{
    public sealed record CreateEntityCommand(string SolutionDirectory, Entity Entity) : IRequest;

    public class CreateEntityHandler(ICraftsmanUtilities utilities, IScaffoldingDirectoryStore scaffoldingDirectoryStore)
        : IRequestHandler<CreateEntityCommand>
    {
        public Task Handle(CreateEntityCommand request, CancellationToken cancellationToken)
        {
            var classPath = ClassPathHelper.EntityClassPath(scaffoldingDirectoryStore.SrcDirectory, $"{request.Entity.Name}.cs", request.Entity.Plural, scaffoldingDirectoryStore.ProjectBaseName);
            var fileText = GetEntityFileText(classPath.ClassNamespace, request.SolutionDirectory, scaffoldingDirectoryStore.SrcDirectory, request.Entity, scaffoldingDirectoryStore.ProjectBaseName);
            utilities.CreateFile(classPath, fileText);
            return Task.CompletedTask;
        }
    }

    public sealed record CreateBaseEntityCommand(bool UseSoftDelete) : IRequest;

    public class CreateBaseEntityHandler(ICraftsmanUtilities utilities, IScaffoldingDirectoryStore scaffoldingDirectoryStore)
        : IRequestHandler<CreateBaseEntityCommand>
    {
        public Task Handle(CreateBaseEntityCommand request, CancellationToken cancellationToken)
        {
            var classPath = ClassPathHelper.EntityClassPath(scaffoldingDirectoryStore.SrcDirectory, $"BaseEntity.cs", "", scaffoldingDirectoryStore.ProjectBaseName);
            var fileText = GetBaseEntityFileText(classPath.ClassNamespace, request.UseSoftDelete);
            utilities.CreateFile(classPath, fileText);
            return Task.CompletedTask;
        }
    }

    public sealed record CreateUserEntityCommand(Entity Entity) : IRequest;

    public class CreateUserEntityHandler(ICraftsmanUtilities utilities, IScaffoldingDirectoryStore scaffoldingDirectoryStore)
        : IRequestHandler<CreateUserEntityCommand>
    {
        public Task Handle(CreateUserEntityCommand request, CancellationToken cancellationToken)
        {
            var classPath = ClassPathHelper.EntityClassPath(scaffoldingDirectoryStore.SrcDirectory, $"{request.Entity.Name}.cs", request.Entity.Plural, scaffoldingDirectoryStore.ProjectBaseName);
            var fileText = GetUserEntityFileText(classPath.ClassNamespace, scaffoldingDirectoryStore.SrcDirectory, request.Entity, scaffoldingDirectoryStore.ProjectBaseName);
            utilities.CreateFile(classPath, fileText);
            return Task.CompletedTask;
        }
    }

    public sealed record CreateUserRoleEntityCommand() : IRequest;

    public class CreateUserRoleEntityHandler(ICraftsmanUtilities utilities, IScaffoldingDirectoryStore scaffoldingDirectoryStore)
        : IRequestHandler<CreateUserRoleEntityCommand>
    {
        public Task Handle(CreateUserRoleEntityCommand request, CancellationToken cancellationToken)
        {
            var entityName = "UserRole";
            var classPath = ClassPathHelper.EntityClassPath(scaffoldingDirectoryStore.SrcDirectory, $"{entityName}.cs", "Users", scaffoldingDirectoryStore.ProjectBaseName);
            var fileText = GetUserRoleEntityFileText(classPath.ClassNamespace, scaffoldingDirectoryStore.SrcDirectory, scaffoldingDirectoryStore.ProjectBaseName);
            utilities.CreateFile(classPath, fileText);
            return Task.CompletedTask;
        }
    }

    public sealed record CreateRolePermissionsEntityCommand(Entity Entity) : IRequest;

    public class CreateRolePermissionsEntityHandler(ICraftsmanUtilities utilities, IScaffoldingDirectoryStore scaffoldingDirectoryStore)
        : IRequestHandler<CreateRolePermissionsEntityCommand>
    {
        public Task Handle(CreateRolePermissionsEntityCommand request, CancellationToken cancellationToken)
        {
            var classPath = ClassPathHelper.EntityClassPath(scaffoldingDirectoryStore.SrcDirectory, $"{request.Entity.Name}.cs", request.Entity.Plural, scaffoldingDirectoryStore.ProjectBaseName);
            var fileText = GetRolePermissionsEntityFileText(classPath.ClassNamespace, scaffoldingDirectoryStore.SrcDirectory, scaffoldingDirectoryStore.ProjectBaseName);
            utilities.CreateFile(classPath, fileText);
            return Task.CompletedTask;
        }
    }

    public static string GetEntityFileText(string classNamespace, string solutionDirectory, string srcDirectory, Entity entity, string projectBaseName)
    {
        var creationModelClassName = EntityModel.Creation.GetClassName(entity.Name);
        var updateClassName = EntityModel.Update.GetClassName(entity.Name);
        var propString = EntityPropBuilder(entity.Properties);
        var tableAnnotation = EntityAnnotationBuilder(entity);
        var entityCreatedDomainMessage = FileNames.EntityCreatedDomainMessage(entity.Name);
        var entityUpdatedDomainMessage = FileNames.EntityUpdatedDomainMessage(entity.Name);

        var foreignEntityUsings = "";
        var foreignProps = entity.Properties.Where(e => e.IsForeignKey && !e.GetDbRelationship.IsSelf).ToList();
        foreach (var entityProperty in foreignProps)
        {
            var classPath = ClassPathHelper.EntityClassPath(srcDirectory, $"", entityProperty.ForeignEntityPlural, projectBaseName);
            var modelsClassPath = ClassPathHelper.EntityModelClassPath(srcDirectory, entityProperty.ForeignEntityName, entityProperty.ForeignEntityPlural, null, projectBaseName);

            foreignEntityUsings += $@"
using {classPath.ClassNamespace};
using {modelsClassPath.ClassNamespace};";
        }

        var valueObjectUsings = "";
        var valueObjects = entity.Properties.Where(e => e.IsValueObject).ToList();
        foreach (var entityProperty in valueObjects)
        {
            var voClassPath = ClassPathHelper.ValueObjectClassPath(srcDirectory, entityProperty.ValueObjectType.ClassName, entityProperty.ValueObjectPlural, projectBaseName);
            valueObjectUsings += $@"
using {voClassPath.ClassNamespace};";
        }
        
        var exceptionClassPath = ClassPathHelper.ExceptionsClassPath(srcDirectory, "", projectBaseName);
        var modelClassPath = ClassPathHelper.EntityModelClassPath(srcDirectory, entity.Name, entity.Plural, null, projectBaseName);
        var domainEventsClassPath = ClassPathHelper.DomainEventsClassPath(srcDirectory, "", entity.Plural, projectBaseName);

        var creationModelVarName = creationModelClassName.LowercaseFirstLetter();
        var updateModelVarName = updateClassName.LowercaseFirstLetter();
        var createEntityVar = $"new{entity.Name.UppercaseFirstLetter()}";
        var createPropsAssignment = GetCreatePropsAssignment(entity, createEntityVar, creationModelVarName, creationModelClassName);
        var updatePropsAssignment = GetUpdatePropsAssignment(entity, updateClassName, updateModelVarName);
        
        // lang=csharp
        return $$"""
                 namespace {{classNamespace}};

                 using System.ComponentModel.DataAnnotations;
                 using System.ComponentModel.DataAnnotations.Schema;
                 using Destructurama.Attributed;
                 using {{exceptionClassPath.ClassNamespace}};
                 using {{modelClassPath.ClassNamespace}};
                 using {{domainEventsClassPath.ClassNamespace}};{{foreignEntityUsings}}{{valueObjectUsings}}

                 {{tableAnnotation}}
                 public class {{entity.Name}} : BaseEntity
                 {
                 {{propString}}    // Add Props Marker -- Deleting this comment will cause the add props utility to be incomplete


                     public static {{entity.Name}} Create({{creationModelClassName}} {{creationModelClassName.LowercaseFirstLetter()}})
                     {
                         var {{createEntityVar}} = new {{entity.Name}}();

                 {{createPropsAssignment}}

                         {{createEntityVar}}.QueueDomainEvent(new {{entityCreatedDomainMessage}}(){ {{entity.Name}} = {{createEntityVar}} });
                         
                         return {{createEntityVar}};
                     }

                     public {{entity.Name}} Update({{updateClassName}} {{updateClassName.LowercaseFirstLetter()}})
                     {
                 {{updatePropsAssignment}}

                         QueueDomainEvent(new {{entityUpdatedDomainMessage}}(){ Id = Id });
                         return this;
                     }

                     // Add Prop Methods Marker -- Deleting this comment will cause the add props utility to be incomplete
                     
                     protected {{entity.Name}}() { } // For EF + Mocking
                 }
                 """;
    }

    private static string GetUpdatePropsAssignment(Entity entity, string updateClassName, string updateModelVarName)
    {
        return string.Join($"{Environment.NewLine}", entity.Properties
            .Where(x => (x.IsPrimitiveType && x.GetDbRelationship.IsNone && x.CanManipulate)
                        || x.IsStringArray
                        || x.IsValueObject)
            .Select(property =>
            {
                if (property.IsPrimitiveType && property.GetDbRelationship.IsNone && property.CanManipulate)
                    return $"        {property.Name} = {updateClassName.LowercaseFirstLetter()}.{property.Name};";

                if (property.IsStringArray)
                    return $"        Set{property.Name}({updateClassName.LowercaseFirstLetter()}.{property.Name});";

                if (property.IsValueObject)
                {
                    return property.ValueObjectType
                        .GetEntityUpdateSetter(property.Name, updateModelVarName);
                }

                return string.Empty;
            }));
    }

    private static string GetCreatePropsAssignment(Entity entity, string createEntityVar, string creationModelVarName,
        string creationModelClassName)
    {
        return string.Join($"{Environment.NewLine}", entity.Properties
            .Where(x => (x.IsPrimitiveType && x.GetDbRelationship.IsNone && x.CanManipulate)
                        || x.IsStringArray
                        || x.IsValueObject)
            .Select(property =>
            {
                if (property.IsPrimitiveType && property.GetDbRelationship.IsNone && property.CanManipulate)
                    return
                        $"        {createEntityVar}.{property.Name} = {creationModelVarName}.{property.Name};";

                if (property.IsValueObject)
                {
                    return property.ValueObjectType
                        .GetEntityCreationSetter(createEntityVar, property.Name, creationModelVarName);
                }

                if (property.IsStringArray)
                    return
                        $"        {createEntityVar}.Set{property.Name}({creationModelClassName.LowercaseFirstLetter()}.{property.Name});";

                return string.Empty;
            }));
    }


    public static string GetBaseEntityFileText(string classNamespace, bool useSoftDelete)
    {
        var isDeletedProp = useSoftDelete
            ? $@"
    public bool IsDeleted {{ get; private set; }}"
            : "";

        var isDeletedMethod = useSoftDelete
            ? $@"
    
    public void UpdateIsDeleted(bool isDeleted)
    {{
        IsDeleted = isDeleted;
    }}"
            : "";

        // lang=csharp
        return $$"""
                 namespace {{classNamespace}};

                 using System.ComponentModel.DataAnnotations;
                 using System.ComponentModel.DataAnnotations.Schema;

                 public abstract class BaseEntity
                 {
                     [Key]
                     public Guid Id { get; private set; } = Guid.NewGuid();
                     
                     public DateTimeOffset CreatedOn { get; private set; }
                     
                     public string CreatedBy { get; private set; }
                     
                     public DateTimeOffset? LastModifiedOn { get; private set; }
                     
                     public string LastModifiedBy { get; private set; }{{isDeletedProp}}
                     
                     [NotMapped]
                     public List<DomainEvent> DomainEvents { get; } = new List<DomainEvent>();

                     public void UpdateCreationProperties(DateTimeOffset createdOn, string createdBy)
                     {
                         CreatedOn = createdOn;
                         CreatedBy = createdBy;
                     }
                     
                     public void UpdateModifiedProperties(DateTimeOffset? lastModifiedOn, string lastModifiedBy)
                     {
                         LastModifiedOn = lastModifiedOn;
                         LastModifiedBy = lastModifiedBy;
                     }{{isDeletedMethod}}
                     
                     public void QueueDomainEvent(DomainEvent @event)
                     {
                         if(!DomainEvents.Contains(@event))
                             DomainEvents.Add(@event);
                     }
                 }
                 """;
    }

    public static string EntityAnnotationBuilder(Entity entity)
    {
        if (string.IsNullOrEmpty(entity.TableName))
            return null;

        // must have table name to have a schema :-(
        var tableName = entity.TableName;
        return entity.Schema != null ? @$"[Table(""{tableName}"", Schema=""{entity.Schema}"")]" : @$"[Table(""{tableName}"")]";
    }

    public static string EntityPropBuilder(List<EntityProperty> props)
    {
        var propString = "";
        foreach (var property in props.Where(x => x.GetDbRelationship.IsNone))
        {
            var attributes = AttributeBuilder(property);
            
            if (property.ValueObjectType.IsNone)
            {
                propString += attributes;
                var defaultValue = GetDefaultValueText(property.DefaultValue, property);
                propString += property.GetDbRelationship.GetPrincipalPropString(property.Type, 
                    property.Name, 
                    defaultValue, 
                    property.ForeignEntityName, 
                    property.ForeignEntityPlural,
                    
                    // these can be null because practically we only need it for children
                    null,
                    null);
            }
            else
            {
                propString += attributes;
                propString += $@"   public {property.ValueObjectName} {property.Name} {{ get; private set; }}{Environment.NewLine}{Environment.NewLine}";
            }
        }

        return propString;
    }

    private static string GetDefaultValueText(string defaultValue, EntityProperty prop)
    {
        if (prop.Type == "string")
            return defaultValue == null ? "" : @$" = ""{defaultValue}"";";

        if ((prop.Type.IsGuidPropertyType() && !prop.Type.Contains("?") && !prop.IsForeignKey))
            return !string.IsNullOrEmpty(defaultValue) ? @$" = Guid.Parse(""{defaultValue}"");" : "";

        if (prop.IsStringArray)
            return " = [];";

        return string.IsNullOrEmpty(defaultValue) ? "" : $" = {defaultValue};";
    }

    private static string AttributeBuilder(EntityProperty entityProperty)
    {
        var attributeString = "";
        if (entityProperty.IsRequired)
            attributeString += @$"    [Required]{Environment.NewLine}";
    
        if (!entityProperty.IsPrimitiveType && !entityProperty.IsStringArray && !entityProperty.IsValueObject)
            attributeString += $@"    [JsonIgnore, IgnoreDataMember]{Environment.NewLine}";
        attributeString += ColumnAttributeBuilder(entityProperty);
        
        if(entityProperty.IsLogMasked)
            attributeString += $@"    [LogMasked]{Environment.NewLine}";

        return attributeString;
    }

    private static string ColumnAttributeBuilder(EntityProperty entityProperty)
    {
        if (!string.IsNullOrEmpty(entityProperty.ColumnName) || !string.IsNullOrEmpty(entityProperty.ColumnType))
        {
            var columnString = "";
            if (!string.IsNullOrEmpty(entityProperty.ColumnName))
                columnString += @$"""{entityProperty.ColumnName}""";

            if (!string.IsNullOrEmpty(entityProperty.ColumnType))
            {
                if (!string.IsNullOrEmpty(entityProperty.ColumnName))
                    columnString += ",";

                columnString += @$"TypeName = ""{entityProperty.ColumnType}""";
            }

            return @$"    [Column({columnString})]{Environment.NewLine}";
        }

        return "";
    }

    public static string GetUserEntityFileText(string classNamespace, string srcDirectory, Entity entity, string projectBaseName)
    {
        var dtoClassPath = ClassPathHelper.DtoClassPath(srcDirectory, $"", entity.Plural, projectBaseName);
        var domainEventsClassPath = ClassPathHelper.DomainEventsClassPath(srcDirectory, "", entity.Plural, projectBaseName);
        var emailsClassPath = ClassPathHelper.EntityClassPath(srcDirectory, $"", "Emails", projectBaseName);
        var exceptionClassPath = ClassPathHelper.ExceptionsClassPath(srcDirectory, "", projectBaseName);
        var modelClassPath = ClassPathHelper.EntityModelClassPath(srcDirectory, "User", "Users", null, projectBaseName);

        // lang=csharp
        return $$"""
                 namespace {{classNamespace}};

                 using {{exceptionClassPath.ClassNamespace}};
                 using {{dtoClassPath.ClassNamespace}};
                 using {{domainEventsClassPath.ClassNamespace}};
                 using {{emailsClassPath.ClassNamespace}};
                 using {{modelClassPath.ClassNamespace}};
                 using Roles;
                 using System.Text.Json.Serialization;
                 using System.Runtime.Serialization;

                 public class User : BaseEntity
                 {
                     public string Identifier { get; private set; }

                     public string FirstName { get; private set; }

                     public string LastName { get; private set; }

                     public Email Email { get; private set; }

                     public string Username { get; private set; }

                     [JsonIgnore]
                     [IgnoreDataMember]
                     public ICollection<UserRole> Roles { get; private set; } = new List<UserRole>();

                     // Add Props Marker -- Deleting this comment will cause the add props utility to be incomplete


                     public static User Create(UserForCreation userForCreation)
                     {
                         ValidationException.ThrowWhenNullOrWhitespace(userForCreation.Identifier, 
                             "Please provide an identifier.");

                         var newUser = new User();

                         newUser.Identifier = userForCreation.Identifier;
                         newUser.FirstName = userForCreation.FirstName;
                         newUser.LastName = userForCreation.LastName;
                         newUser.Email = new Email(userForCreation.Email);
                         newUser.Username = userForCreation.Username;

                         newUser.QueueDomainEvent(new UserCreated(){ User = newUser });
                         
                         return newUser;
                     }

                     public User Update(UserForUpdate userForUpdate)
                     {
                         ValidationException.ThrowWhenNullOrWhitespace(userForUpdate.Identifier, 
                             "Please provide an identifier.");

                         Identifier = userForUpdate.Identifier;
                         FirstName = userForUpdate.FirstName;
                         LastName = userForUpdate.LastName;
                         Email = new Email(userForUpdate.Email);
                         Username = userForUpdate.Username;

                         QueueDomainEvent(new UserUpdated(){ Id = Id });
                         return this;
                     }

                     // Add Prop Methods Marker -- Deleting this comment will cause the add props utility to be incomplete

                     public UserRole AddRole(Role role)
                     {
                         var newList = Roles.ToList();
                         var userRole = UserRole.Create(this, role);
                         newList.Add(userRole);
                         UpdateRoles(newList);
                         return userRole;
                     }

                     public UserRole RemoveRole(Role role)
                     {
                         var newList = Roles.ToList();
                         var roleToRemove = Roles.FirstOrDefault(x => x.Role == role);
                         newList.Remove(roleToRemove);
                         UpdateRoles(newList);
                         return roleToRemove;
                     }

                     private void UpdateRoles(IList<UserRole> updates)
                     {
                         var additions = updates.Where(userRole => Roles.All(x => x.Role != userRole.Role)).ToList();
                         var removals = Roles.Where(userRole => updates.All(x => x.Role != userRole.Role)).ToList();
                     
                         var newList = Roles.ToList();
                         removals.ForEach(toRemove => newList.Remove(toRemove));
                         additions.ForEach(newRole => newList.Add(newRole));
                         Roles = newList;
                         QueueDomainEvent(new UserRolesUpdated(){ UserId = Id });
                     }
                     
                     protected User() { } // For EF + Mocking
                 }
                 """;
    }
    

    public static string GetUserRoleEntityFileText(string classNamespace, string srcDirectory, string projectBaseName)
    {
        var domainEventsClassPath = ClassPathHelper.DomainEventsClassPath(srcDirectory, "", "Users", projectBaseName);

        // lang=csharp
        return $$"""
                 namespace {{classNamespace}};

                 using {{domainEventsClassPath.ClassNamespace}};
                 using System.ComponentModel.DataAnnotations.Schema;
                 using System.Runtime.Serialization;
                 using System.Text.Json.Serialization;
                 using Roles;

                 public class UserRole : BaseEntity
                 {
                     public User User { get; private set; }
                     public Role Role { get; private set; }

                     // Add Props Marker -- Deleting this comment will cause the add props utility to be incomplete
                     

                     public static UserRole Create(User user, Role role)
                     {
                         var newUserRole = new UserRole
                         {
                             User = user,
                             Role = role
                         };

                         newUserRole.QueueDomainEvent(new UserRolesUpdated(){ UserId = user.Id });
                         
                         return newUserRole;
                     }

                     // Add Prop Methods Marker -- Deleting this comment will cause the add props utility to be incomplete
                     
                     protected UserRole() { } // For EF + Mocking
                 }
                 """;
    }
    

    public static string GetRolePermissionsEntityFileText(string classNamespace, string srcDirectory, string projectBaseName)
    {
        var exceptionClassPath = ClassPathHelper.ExceptionsClassPath(srcDirectory, "", projectBaseName);
        var modelClassPath = ClassPathHelper.EntityModelClassPath(srcDirectory, "RolePermission", "RolePermissions", null, projectBaseName);
        
        // lang=csharp
        return $$"""
                 namespace {{classNamespace}};

                 using Dtos;
                 using DomainEvents;
                 using Roles;
                 using Domain;
                 using {{exceptionClassPath.ClassNamespace}};
                 using {{modelClassPath.ClassNamespace}};

                 public class RolePermission : BaseEntity
                 {
                     public Role Role { get; private set; }
                     public string Permission { get; private set; }

                     // Add Props Marker -- Deleting this comment will cause the add props utility to be incomplete


                     public static RolePermission Create(RolePermissionForCreation rolePermissionForCreation)
                     {
                         ValidationException.Must(BeAnExistingPermission(rolePermissionForCreation.Permission), 
                             "Please use a valid permission.");

                         var newRolePermission = new RolePermission();

                         newRolePermission.Role = new Role(rolePermissionForCreation.Role);
                         newRolePermission.Permission = rolePermissionForCreation.Permission;

                         newRolePermission.QueueDomainEvent(new RolePermissionCreated(){ RolePermission = newRolePermission });
                         
                         return newRolePermission;
                     }

                     public RolePermission Update(RolePermissionForUpdate rolePermissionForUpdate)
                     {
                         ValidationException.Must(BeAnExistingPermission(rolePermissionForUpdate.Permission), 
                             "Please use a valid permission.");

                         Role = new Role(rolePermissionForUpdate.Role);
                         Permission = rolePermissionForUpdate.Permission;

                         QueueDomainEvent(new RolePermissionUpdated(){ Id = Id });
                         return this;
                     }

                     // Add Prop Methods Marker -- Deleting this comment will cause the add props utility to be incomplete
                     
                     private static bool BeAnExistingPermission(string permission)
                     {
                         return Permissions.List().Contains(permission, StringComparer.InvariantCultureIgnoreCase);
                     }
                     
                     protected RolePermission() { } // For EF + Mocking
                 }
                 """;
    }
}